using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using ICSharpCode.SharpZipLib.Zip;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

public class AssetUpdater : API.IAssetUpdater
{
    public void SetNotifyHandler(API.IUpdateNotify handler)
    {
        notifyHandle = handler;
    }
    public void StartUpdate(string server, int port)
    {
        DoUpdate(server, port);
    }

    public void SetFilter(Regex[] regexFilters)
    {
        this.regexFilters = regexFilters;
    }
    //-/////////////////////////////////////////

    bool isConnecting;
    mb.ILog logger;
    API.IUpdateNotify notifyHandle;
    Regex[] regexFilters;
    bool fullUpdate;

    public AssetUpdater()
    {
        logger = mb.ServiceLocator.Instance.Get<mb.ILog>();
        fullUpdate = false;
    }

    private string lastSeg(string str) { return str.Substring(str.LastIndexOf('/') + 1); }
    private async Task<bool> unzip(string zipFilename, string targetDirectory, int bufferSize = 256 * 1024)
    {
        logger.Log("extrac zip", zipFilename);
        ICSharpCode.SharpZipLib.Zip.ZipConstants.DefaultCodePage = System.Text.Encoding.UTF8.CodePage;
        try {
            notifyHandle?.notifyProgress(0);
            Directory.CreateDirectory(targetDirectory);
            long readTotal = 0;
            long sizeTotal = 0;
            using( FileStream fileStream = new FileStream(zipFilename, FileMode.Open) ) {
                byte[] dataBuffer = new byte[bufferSize];
                ZipFile zipFile = new ZipFile(fileStream);
                foreach( ZipEntry entry in zipFile ) sizeTotal += entry.Size;
                foreach( ZipEntry entry in zipFile ) {
                    string targetFile = Path.Combine(targetDirectory, entry.Name);
                    logger.Log("extract", targetFile);
                    if( entry.IsFile ) {
                        var dir = targetFile.Substring(0, targetFile.LastIndexOf('/'));
                        //如果新创建的目录原来是一个文件，创建就会失败，必须先检查这种情况，把原来的文件删除
                        if( File.Exists(dir) ) File.Delete(dir);
                        Directory.CreateDirectory(dir);
                        using( FileStream outputFile = File.Create(targetFile) ) {
                            if( entry.Size > 0 ) {
                                Stream zippedStream = zipFile.GetInputStream(entry);
                                int readBytes;
                                while( ( readBytes = zippedStream.Read(dataBuffer, 0, bufferSize) ) > 0 ) {
                                    outputFile.Write(dataBuffer, 0, readBytes);
                                    outputFile.Flush();
                                    readTotal += readBytes;
                                }
                            }
                        }
                        notifyHandle?.notifyProgress(1.0f * readTotal / sizeTotal);
                        await Task.Delay(TimeSpan.FromMilliseconds(1));
                    }
                }
            }
            notifyHandle?.notifyProgress(100);
            return true;
        }
        catch( System.Exception e ) {
            logger.Log(e);
            return false;
        }
    }

    void asureFileDirectory(string file)
    {
        try {

            var dir = Directory.GetParent(file);
            if( !dir.Exists )
                Directory.CreateDirectory(dir.FullName);
        }
        catch( Exception e ) {
            throw e;
        }

    }

    private async Task<bool> download(string url, string saveAs, string post = null, Action<float> onProgress = null)
    {

        logger.Log("download", url, saveAs, post);

        asureFileDirectory(saveAs);
        var fs = File.Create(saveAs);
        var req = WebRequest.Create(url) as HttpWebRequest;

        if( post != null ) {
            req.Method = "POST";
            req.ContentType = "text/plain";
            req.Accept = "application/octet-stream";
            var body = Encoding.UTF8.GetBytes(post);
            using( var ps = await req.GetRequestStreamAsync() ) {
                await ps.WriteAsync(body, 0, body.Length);
            }
        }
        else {
            req.Method = "GET";
        }

        try {
            var res = await req.GetResponseAsync();
            var stream = res.GetResponseStream();
            int BufferSize = 1024 * 1024;
            byte[] buffer = new byte[BufferSize];
            int totalBytes = (int) res.ContentLength;
            int receivedBytes = 0;

            for(; ; )
            {
                int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);
                if( bytesRead == 0 ) {
                    await Task.Yield();
                    break;
                }

                fs.Write(buffer, 0, bytesRead);
                receivedBytes += bytesRead;
                SetProgress(1.0f * receivedBytes / totalBytes);
            }
        }
        catch( Exception e ) {
            logger.Log("download fail", e);
            return false;

        }

        fs.Close();
        SetProgress(1);
        return true;
    }

    void SetProgress(float percent)
    {
        if( notifyHandle != null )
            notifyHandle.notifyProgress(percent);
    }
    void SetTip(string tip)
    {
        logger.Log("settip", tip);
        if( notifyHandle != null )
            notifyHandle.notifyDescription(tip);
    }

    //以下结构与nodejs中生成的filelist对应
    class FileInfo
    {
        public ulong size { get; set; }
        public string path { get; set; }
        public string md5 { get; set; }
    }
    class FileListInfo
    {
        public List<FileInfo> files { get; set; } = new List<FileInfo>();
    }

    private void calcUpdateFiles(string newPath, string oldPath, List<string> needUpdate, List<string> needRemove, FileListInfo remainFileInfo)
    {

        FileListInfo oldInfo = new FileListInfo();
        if( File.Exists(oldPath) && !fullUpdate ) {
            oldInfo = JsonConvert.DeserializeObject<FileListInfo>(File.ReadAllText(oldPath));
        }
        FileListInfo newInfo = JsonConvert.DeserializeObject<FileListInfo>(File.ReadAllText(newPath));
        newInfo.files = newInfo.files.Where((fi, b) => this.notifyHandle.isFileNeed(fi.path)).ToList();

        var oldFileMap = oldInfo.files.ToDictionary(fi => fi.path);
        var newFileMap = newInfo.files.ToDictionary(fi => fi.path);

        needUpdate.AddRange(newInfo.files.Where(fi =>
        {
            bool has = oldFileMap.ContainsKey(fi.path);
            if( !has ) return true;
            // if (File.Exists(notifyHandle.g))
            var ofi = oldFileMap[fi.path];
            return ofi.md5 != fi.md5;
        }).Select(fi => fi.path));

        needRemove.AddRange(oldInfo.files.Where(fi =>
        {
            if( !this.notifyHandle.shouldFileRemove(fi.path) ) return false;
            return !newFileMap.ContainsKey(fi.path);
        }).Select(fi => fi.path));

        var remainFileMap = oldInfo.files.ToDictionary(fi => fi.path);
        needRemove.ForEach(path => { remainFileMap.Remove(path); });
        needUpdate.ForEach(path => { remainFileMap[path] = newFileMap[path]; });
        remainFileInfo.files.AddRange(remainFileMap.Select(v => v.Value));
    }

    async void DoUpdate(string server, int port)
    {
        if( isConnecting ) {
            logger.Log("AssetUpdate is already updating");
            return;
        }
        logger.Log("------start update!!!", server);
        this.isConnecting = true;
        notifyHandle.notifyBegin();

        string url = $"http://{server}:{port}/filelist";
        string storageRoot = notifyHandle.getStorageRoot();
        string oldFLPath = $"{storageRoot}/{lastSeg(url)}";
        string newFLPath = oldFLPath + ".new";

        SetTip("检查更新状态");
        SetProgress(0);
        bool ok = await download(url, newFLPath);
        if( !ok ) {
            notifyHandle.notifyEnd(false, "检查更新失败，请重试");
            this.isConnecting = false;
            return;
        }

        SetTip("计算更新文件");

        List<string> needUpdate = new List<string>();
        List<string> needRemove = new List<string>();
        FileListInfo remainFileInfo = new FileListInfo();
        calcUpdateFiles(newFLPath, oldFLPath, needUpdate, needRemove, remainFileInfo);
        if( needUpdate.Count != 0 ) {
            var reqUpdateBody = String.Join(";", needUpdate);
            logger.Log("update files", reqUpdateBody);
            SetTip("请求更新包");
            url = $"http://{server}:{port}/getfiles";
            string zipPath = $"{Xamarin.Essentials.FileSystem.CacheDirectory}/updateAsset.zip";
            ok = await download(url, zipPath, reqUpdateBody, p => { notifyHandle.notifyProgress(p); });
            if( !ok ) {
                notifyHandle.notifyEnd(false, "获取更新包失败，请重试");
                this.isConnecting = false;
                return;
            }


            string decodeAs = $"{storageRoot}/";
            if( fullUpdate ) {
                Directory.Delete(decodeAs, true);
            }
            SetTip("安装更新包");
            ok = await unzip(zipPath, decodeAs, 1024 * 1024 * 1);
            if( !ok ) {
                notifyHandle.notifyEnd(false, "安装更新包失败，请重试");
                this.isConnecting = false;
                return;
            }
            File.Delete(zipPath);
        }

        if( needRemove.Count != 0 ) {
            needRemove.ForEach((name) =>
            {
                string path = $"{storageRoot}/{name}";
                if( !notifyHandle.shouldFileRemove(path) ) return;
                try {
                    File.Delete(path);
                    logger.Log("remove file", path);
                }
                catch( System.Exception ) {
                    // logger.Log ("remove file failed, ignore", path);
                }
            });
        }

        if( File.Exists(oldFLPath) ) File.Delete(oldFLPath);
        if( File.Exists(newFLPath) ) File.Delete(newFLPath);
        File.WriteAllText(oldFLPath, JsonConvert.SerializeObject(remainFileInfo, Formatting.Indented));

        SetProgress(1);
        SetTip("更新完成，即将进入");
        await Task.Delay(TimeSpan.FromMilliseconds(10));

        this.isConnecting = false;
        notifyHandle.notifyEnd(true);
    }

    public void SetFullUpdate(bool b)
    {
        fullUpdate = b;
    }
}